<?php
/**
 * @copyright Copyright (c) 2019
 */

namespace lujie\proxy;

use Workerman\Connection\AsyncTcpConnection;
use Workerman\Connection\TcpConnection;
use Workerman\Worker;

/**
 * Class Socks5Server
 * @package lujie\proxy
 * @author Lujie Zhou <gao_lujie@live.cn>
 */
class Socks5ProxyServer extends Worker
{
    public $name = 'Socks5ProxyServer';

    public $authEnabled = true;
    public $username = 'Socks5ProxyUsername';
    public $password = 'Socks5ProxyPassword';

    public const CMD_CONNECT = 1;
    public const CMD_BIND = 2;
    public const CMD_UDP_ASSOCIATE = 3;

    public const METHOD_NO_AUTH = 0;
    public const METHOD_GSS_API = 1;
    public const METHOD_USER_PASS = 2;

    /**
     * @inheritdoc
     */
    public function run()
    {
        $this->onConnect = [$this, 'onConnect'];
        $this->onMessage = [$this, 'onMessage'];
        parent::run();
    }

    /**
     * @param TcpConnection $connection
     * @inheritdoc
     */
    public function onConnect(TcpConnection $connection): void
    {
        $connection->stage = ProxyConst::STAGE_INIT;
    }

    /**
     * @param TcpConnection $connection
     * @param string $buffer
     * @inheritdoc
     * @throws \Exception
     */
    public function onMessage(TcpConnection $connection, string $buffer): void
    {
        switch ($connection->stage) {
            case ProxyConst::STAGE_INIT:
                if ($this->authEnabled) {
                    $methodsLen = ord($buffer[1]);
                    $methods = [];
                    for ($i = 0; $i < strlen($buffer) - 3; $i++) {
                        array_push($methods, ord($buffer[$i + 3]));
                    }
                    if (in_array(self::METHOD_USER_PASS, $methods, true)) {
                        $connection->send("\x05\x02");
                        $connection->stage = ProxyConst::STAGE_AUTH;
                        return;
                    }
                    echo "client does not support user/pass auth\n";
                    $connection->send("\x05\xff");
                    $connection->stage = ProxyConst::STAGE_DESTROYED;
                    $connection->close();
                    return;
                }
                $connection->send("\x05\x00");
                $connection->stage = ProxyConst::STAGE_ADDR;
                return;
            case ProxyConst::STAGE_AUTH:
                $userLen = ord($buffer[1]);
                $user = substr($buffer, 2, $userLen);
                $passLen = ord($buffer[2 + $userLen]);
                $pass = substr($buffer, 3 + $userLen, $passLen);
                if ($user == $this->username && $pass == $this->password) {
                    $connection->send("\x05\x00");
                    $connection->stage = ProxyConst::STAGE_ADDR;
                    return;
                }
                echo "auth failed\n";
                $connection->send("\x05\x01");
                $connection->stage = ProxyConst::STAGE_DESTROYED;
                $connection->close();
                return;
            case ProxyConst::STAGE_ADDR:
                $cmd = ord($buffer[1]);
                if ($cmd != self::CMD_CONNECT) {
                    echo "bad cmd $cmd\n";
                    $connection->close();
                    return;
                }
                $header_data = Socks5Helper::parseHeader($buffer);
                if (!$header_data) {
                    $connection->close();
                    return;
                }
                $connection->stage = ProxyConst::STAGE_CONNECTING;
                $remote_connection = new AsyncTcpConnection('tcp://' . $header_data[1] . ':' . $header_data[2]);
                $remote_connection->onConnect = function ($remote_connection) use ($connection) {
                    $connection->state = ProxyConst::STAGE_STREAM;
                    $connection->send("\x05\x00\x00\x01\x00\x00\x00\x00\x10\x10");
                    $connection->pipe($remote_connection);
                    $remote_connection->pipe($connection);
                };
                $remote_connection->connect();
        }
    }
}